"""Automation job implementations used by the project scheduler."""
from __future__ import annotations

from datetime import date, datetime, timedelta
from typing import Dict, List, Optional, Tuple

from app.backtest.engine import BacktestEngine, BtConfig
from app.ingest.coverage import ensure_data_coverage
from app.ingest.news import ingest_latest_news
from app.utils import alerts
from app.utils.calendar import is_trading_day, previous_trading_day
from app.utils.config import AppConfig, SchedulerSettings, get_config
from app.utils.data_quality import evaluate_data_quality
from app.utils.logging import get_logger
from app.utils.portfolio import get_candidate_pool, list_positions

LOGGER = get_logger(__name__)
LOG_EXTRA = {"stage": "scheduler.jobs"}


def _resolve_scheduler_settings(cfg: AppConfig) -> SchedulerSettings:
    scheduler = getattr(cfg, "scheduler", None)
    if isinstance(scheduler, SchedulerSettings):
        return scheduler
    return SchedulerSettings()


def _normalize_reference_date(reference: Optional[date | datetime] = None) -> date:
    if isinstance(reference, datetime):
        return reference.date()
    if isinstance(reference, date):
        return reference
    return date.today()


def _latest_completed_trading_day(day: date) -> date:
    """Return the latest trading day on or before the given day."""

    if is_trading_day(day):
        return day
    current = day - timedelta(days=1)
    for _ in range(14):
        if is_trading_day(current):
            return current
        current -= timedelta(days=1)
    return day


def _collect_universe(
    trade_date: date,
    *,
    include_positions: bool,
    limit: int,
) -> Tuple[List[str], bool]:
    """Return universe codes aggregated from candidate pool and positions."""

    trade_date_str = trade_date.isoformat()
    candidates, fallback_used = get_candidate_pool(trade_date=trade_date_str, limit=limit)
    universe: List[str] = []
    for candidate in candidates:
        if candidate.ts_code:
            universe.append(candidate.ts_code)
    if include_positions:
        try:
            for position in list_positions(active_only=True):
                if position.ts_code:
                    universe.append(position.ts_code)
        except Exception:  # noqa: BLE001
            LOGGER.exception("读取持仓列表失败，继续仅使用候选池。", extra=LOG_EXTRA)
    unique_universe = list(dict.fromkeys(universe))
    if limit > 0:
        unique_universe = unique_universe[:limit]
    return unique_universe, fallback_used


def run_daily_data_refresh(reference: Optional[date | datetime] = None) -> date:
    """Run the end-of-day data refresh covering TuShare and news ingestion."""

    cfg = get_config()
    scheduler = _resolve_scheduler_settings(cfg)
    ref_day = _normalize_reference_date(reference)
    trade_day = ref_day if is_trading_day(ref_day) else previous_trading_day(ref_day)

    LOGGER.info("启动每日数据拉取任务 trade_day=%s", trade_day, extra=LOG_EXTRA)
    try:
        ensure_data_coverage(
            trade_day,
            trade_day,
            include_limits=scheduler.daily_ingest.include_limits,
            include_extended=scheduler.daily_ingest.include_extended,
            include_news=scheduler.daily_ingest.include_news,
            force=False,
        )
        if scheduler.daily_ingest.include_news:
            ingest_latest_news(days_back=1, force=False)
        alerts.clear_warnings("scheduler.daily_ingest")
        LOGGER.info("每日数据拉取完成 trade_day=%s", trade_day, extra=LOG_EXTRA)
    except Exception as exc:  # noqa: BLE001
        LOGGER.exception("每日数据拉取失败 trade_day=%s", trade_day, extra=LOG_EXTRA)
        alerts.add_warning(
            "scheduler.daily_ingest",
            "每日收盘数据拉取失败",
            str(exc),
            level="error",
            tags=["scheduler", "data_refresh"],
        )
        raise
    return trade_day


def run_preopen_plan(reference: Optional[date | datetime] = None) -> Dict[str, object]:
    """Generate the per-symbol plan using the latest available trading day."""

    cfg = get_config()
    scheduler = _resolve_scheduler_settings(cfg)
    ref_day = _normalize_reference_date(reference)
    if not is_trading_day(ref_day):
        LOGGER.info("当前日期非交易日，跳过开盘前计划生成 date=%s", ref_day, extra=LOG_EXTRA)
        return {"skipped": True, "reason": "non_trading_day", "reference_date": ref_day.isoformat()}

    trade_day = previous_trading_day(ref_day)
    if trade_day >= ref_day:
        trade_day = _latest_completed_trading_day(ref_day - timedelta(days=1))

    LOGGER.info(
        "开始执行开盘前计划 trade_day=%s reference=%s",
        trade_day,
        ref_day,
        extra=LOG_EXTRA,
    )

    universe, fallback_used = _collect_universe(
        trade_day,
        include_positions=scheduler.preopen_plan.include_positions,
        limit=max(1, scheduler.preopen_plan.max_universe),
    )

    if not universe:
        LOGGER.warning("未找到可用的候选标的，跳过开盘前计划 trade_day=%s", trade_day, extra=LOG_EXTRA)
        return {
            "skipped": True,
            "reason": "empty_universe",
            "trade_date": trade_day.isoformat(),
        }

    portfolio_cfg = getattr(cfg, "portfolio", None)
    initial_capital = getattr(portfolio_cfg, "initial_capital", 1_000_000.0)
    max_position = getattr(portfolio_cfg, "max_position", 0.2)
    params = {
        "initial_capital": float(initial_capital),
        "max_position_weight": float(max_position),
    }

    bt_cfg = BtConfig(
        id=f"preopen_plan_{trade_day.strftime('%Y%m%d')}",
        name=f"Pre-open Plan {trade_day.isoformat()}",
        start_date=trade_day,
        end_date=trade_day,
        universe=universe,
        params=params,
    )

    engine = BacktestEngine(bt_cfg, persist_results=True)

    try:
        engine.run()
        alerts.clear_warnings("scheduler.preopen_plan")
        LOGGER.info(
            "开盘前计划生成完成 trade_day=%s symbols=%d fallback=%s",
            trade_day,
            len(universe),
            fallback_used,
            extra=LOG_EXTRA,
        )
        return {
            "trade_date": trade_day.isoformat(),
            "universe": universe,
            "fallback_used": fallback_used,
            "symbol_count": len(universe),
        }
    except Exception as exc:  # noqa: BLE001
        LOGGER.exception("开盘前计划生成失败 trade_day=%s", trade_day, extra=LOG_EXTRA)
        alerts.add_warning(
            "scheduler.preopen_plan",
            "开盘前计划生成失败",
            str(exc),
            level="error",
            tags=["scheduler", "planning"],
        )
        raise


def run_strategy_review(reference: Optional[date | datetime] = None) -> Dict[str, object]:
    """Run the strategy effectiveness review and alert when action is needed."""

    cfg = get_config()
    scheduler = _resolve_scheduler_settings(cfg)
    summary = evaluate_data_quality(window_days=scheduler.strategy_review.window_days)
    result = summary.as_dict()
    LOGGER.info(
        "策略评估完成 score=%s blockers=%s warnings=%s",
        summary.score,
        len(summary.blocking),
        len(summary.warnings),
        extra=LOG_EXTRA,
    )

    if summary.blocking or summary.score < scheduler.strategy_review.score_threshold:
        detail_lines: List[str] = []
        if summary.blocking:
            headline = ", ".join(item.detail for item in summary.blocking[:3])
            detail_lines.append(f"阻塞项: {headline}")
        elif summary.warnings:
            headline = ", ".join(item.detail for item in summary.warnings[:3])
            detail_lines.append(f"警告项: {headline}")
        detail_lines.append(f"得分: {summary.score}")
        detail_text = "\n".join(detail_lines)
        alerts.add_warning(
            "scheduler.strategy_review",
            "策略有效性下降，请复核参数设置",
            detail_text,
            level="warning",
            tags=["scheduler", "strategy_review"],
            payload=result,
        )
    else:
        alerts.clear_warnings("scheduler.strategy_review")

    return result


__all__ = [
    "run_daily_data_refresh",
    "run_preopen_plan",
    "run_strategy_review",
]
